/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package com.chasekernan.hxnova.server;

import com.chasekernan.hxnova.core.dataholders.Global;
import com.chasekernan.hxnova.core.dataholders.SalvageHolder;
import com.chasekernan.hxnova.core.dataholders.Map;
import com.chasekernan.hxnova.core.dataholders.FleetHolder;
import com.chasekernan.hxnova.core.dataholders.MessageHandler;
import com.chasekernan.hxnova.core.designs.HullDesign;
import com.chasekernan.hxnova.core.fleets.Fleet;
import com.chasekernan.hxnova.core.fleets.Salvage;
import com.chasekernan.hxnova.core.players.Player;
import com.chasekernan.hxnova.core.players.PlayerRelations;
import com.chasekernan.hxnova.core.players.PlayerTurnFile;
import com.chasekernan.hxnova.core.players.TurnOrders;
import com.chasekernan.hxnova.core.stars.ProductionQueue;
import com.chasekernan.hxnova.server.functions.CargoTasks;
import com.chasekernan.hxnova.server.functions.Colonizations;
import com.chasekernan.hxnova.server.functions.FleetMovement;
import com.chasekernan.hxnova.server.functions.SalvageDecay;
import com.chasekernan.hxnova.server.functions.Scanning;
import com.chasekernan.hxnova.server.functions.Scrapping;
import com.chasekernan.hxnova.server.functions.battles.Battles;
import com.chasekernan.hxnova.server.functions.Refueling;
import com.chasekernan.hxnova.server.functions.Production;
import com.chasekernan.hxnova.utils.Timing;

import haxe.Serializer;
import haxe.Unserializer;
/**
    Contains all the methods necessary to run a HxNova server.
**/
class Server {
    
    /**
        File extension used when saving a map file.
    **/
    public static var MAP_EXTENSION = "hxnova-map";
    
    /**
        File extension used when saving a turn file.
    **/
    public static var PLAYER_TURN_EXTENSION = "hxnova-turn";
    
    /**
        File extension used when loading a turn order.
    **/
    public static var PLAYER_TURN_ORDERS_EXTENSION = "hxnova-orders";
    
    public static var HOST_FILE_EXTENSION = "hxnova-host";
    
    /**
        The game's default starting year.
    **/
    public static inline var STARTING_YEAR : Int = 2400;
    
    /**
        Sorted by player id.
    **/
    public static var playerTurns : Array<PlayerTurnFile>;
    public static var hostFile : HostFile;
    
    
    /**
        Takes a game made by the [Game.createNewGame] method and creates the 
        inital files for the game (i.e. map, player turn files, host file, etc.
    **/
    public static function initGame(g : Game) {
        gameName = g.name;
        
        //Start new host file
        hostFile = new HostFile(g.name);
        
        Global.messages = new MessageHandler();
        Global.salvage = new SalvageHolder();
        
        //save map file
        IO.makeFolder(g.name);
        IO.writeToFile(IO.compress(Map.writeToXml(Global.map).toString()), 
                g.name + "/" + g.name + "." + MAP_EXTENSION);
                
        Timing.record("saving map");
        
        Global.year = STARTING_YEAR;
        
        createPlayerTurns();
        
        for(player in Global.players.players) {
            Global.messages.sendMessage({to : Target.PlayerTarget(player), 
                    from : Target.Host, 
                    data : MessageData.StartingMessage(player.homeworld.name) } );
            player.updateScannerAndDefenseType();
        }
        Timing.record("create player turns");
        
        saveStars();
        Timing.record("save stars");
        saveFleets();
        savePlayerGames();
        Timing.record("save fleets");
        saveTurns();
        Timing.record("save turns");
        
        saveHostTurn();
        Timing.record("save host turn");
    }
    
    private static function createPlayerTurns() {
        playerTurns = new Array();
        for(player in Global.players.players) {
            playerTurns.push(new PlayerTurnFile(player));
        }
    }
    
    //adds stars that are owned by the player (scanning to be added later) to 
    //the player turn file
    private static function saveStars() {
        for(star in Global.stars.stars) {
            if(star.owner != null && 
                    star.owner.id != Global.players.nullPlayer.id) {
                playerTurns[star.owner.id].stars.stars[star.id] = star;
            }
        }
    }
    
    //adds fleets that are owned by the player (scanning to be added later) to 
    //the player turn file
    private static function saveFleets() {
        for(fleet in Global.fleets) {
            if(fleet == null || fleet.owner == null || fleet.owner.id == 
                    Global.players.nullPlayer.id) continue;
            playerTurns[fleet.owner.id].fleets.addParticle(fleet);
        }
    }
    
    private static function savePlayerGames() {
        for (turn in playerTurns) {
            turn.game = Global.game.cloneForPlayer();
        }
    }
    
    //save each turn file (race names are not used here because they can 
    //contain characters not allowed in file names. i.e. player3-game
    private static function saveTurns() {
        for(turn in playerTurns) {
            IO.writeToFile(IO.compress(
                    PlayerTurnFile.writeToXml(turn).toString()), hostFile.name+
                    "/player" + Std.string(turn.player.id) + "-" + 
					hostFile.name + "." + PLAYER_TURN_EXTENSION);
        }
    }
    
    //Saves all data to a host turn file, then adds that turn file to the
    //host file. Finally, it saves the host file.
    private static function saveHostTurn() {
        var hostTurn = new HostTurnFile();
        hostTurn.year = Global.year; //its for the year before...
        hostTurn.stars = Global.stars;
        hostTurn.fleets = Global.fleets;
        hostTurn.players = Global.players;
        hostTurn.messages = Global.messages;
        hostTurn.salvage = Global.salvage;
        hostTurn.playerData = playerTurns.copy();
        hostTurn.game = Global.game;
        
        hostFile.addTurn(hostTurn);
        
        //saves the file to game-name/game-name.hxnova-host
        HostFile.saveHostFile(hostFile);
    }
    
    public static var missingTurns : Array<Player>;
    public static var orders : Array<TurnOrders>;
    public static var gameName : String;
    
    public static function runTurn(gameName : String) {
        Server.gameName = gameName;
        hostFile = HostFile.loadHostFile(gameName);
        Timing.record("load host file");
        MessageHandler.init();
        
        //sets all globals to those of last turn, except year and messages.
        var latestTurn = hostFile.getLatestTurn();
        Global.year = latestTurn.year + 1;
        Global.fleets = latestTurn.fleets;
        Global.players = latestTurn.players;
        Global.stars = latestTurn.stars;
        Global.salvage = latestTurn.salvage;
        Global.game = latestTurn.game;
        Global.messages = new MessageHandler();
        
        //loads each individual player file and if one is missing, then it is 
        //added to the list of missing turns.
        missingTurns = new Array();
        orders = new Array();
        
        for (player in Global.players) {
            //path to game file (game-name/player3-game-name.hxnova
            var path = gameName + "/player" + Std.string(player.id) + 
                    "-" + gameName + "." + PLAYER_TURN_ORDERS_EXTENSION;
            
            //if no turn file is present
            if (!IO.fileExists(path)) {
                missingTurns.push(player);
                var to = new TurnOrders(player);
                to.year = Global.year;
                orders.push(to);
                
                continue;
            }
            
            //try to read the file.
            try {
                var orderXml = Xml.parse(IO.uncompress(IO.readFromFile(path)))
                    .firstChild();
                var order = TurnOrders.readFromXml(orderXml);
                
                //check to make sure the players match.
                if (order.player.id != player.id) {
                    throw "Player in turn orders does not match the file name." 
                            + "Given orders for player " + player.id + "but "
                            + "the turn orders were for player " 
                            + order.player.id + ".";
                }
                
                orders.push(order);
            } catch (e : Dynamic) {
                throw "The orders for player " + player.id + " are corrupted."
                        + "Cannot continue...\n" + e;
            } 
        }
        
        Timing.record("parse orders");
        
        //check each players orders for cheating (or just errors).
        checkOrders();
        Timing.record("check orders");
        
        applyOrders();
        Timing.record("apply orders");
        
        //run through event order
        runEvents();
        Timing.record("run events");
        
        //create turn data
        createPlayerTurns();
        Timing.record("create player turns");
        
        //calculate score and etc...
        
        saveStars();
        Timing.record("save stars");
        saveFleets();
        Timing.record("save fleets");
        saveTurns();
        Timing.record("save turns");
        
        saveHostTurn();
        Timing.record("save host turn");
    }
    
    private static function runEvents() {
        //First determine what fleets are orbiting stars.
        //More efficient if copy of fleet array is made
        var copy = new Array<Fleet>();
        for (fleet in Global.fleets) copy.push(fleet);
        
        for (star in Global.stars.stars) {
            star.orbitingFleets = new Array();
            
            var i = copy.length;
            while (i-- > 0) {
                if (star.location.equals(copy[i].location)) {
                    star.orbitingFleets.push(copy[i]);
                    copy.splice(i, 1);
                }
            }
        }
        
        Scrapping.run();
        CargoTasks.doUnloadTasks();
        Colonizations.run();
        CargoTasks.doLoadTasks();
        FleetMovement.run();
        SalvageDecay.run();
        Production.run();
        Refueling.run();
        Battles.run();
        Scrapping.run();
        CargoTasks.doUnloadTasks();
        Colonizations.run();
        CargoTasks.doLoadTasks();
        
        Scanning.run();
    }
    
    //This checks for specific, obvious errors (or cheats) such as a wrong 
    //year, duplicate entries, or targeting a fleet that the player never
    //had knowledge of. 
    //TODO: Needs cleaning.
    private static function checkOrders() {
        for (orderSet in orders) {
            try {
                var player = orderSet.player;
                
                //check year
                if (orderSet.year != Global.year) {
                    throw "Order set is not current.";
                }
                    
                //check to see if the designs, a specific fleet, a specific
                //star, or the research field are set twice
                var foundDesigns = false;
                var foundResearch = false;
                var foundRelations = false;
                var foundDefaultBP = false;
                var foundBattlePlans = false;
                var foundBattlePlan = 
                        new Array<{fleet : Fleet, design : HullDesign}>();
                var foundFleets = new Array<Int>();
                var foundStars = new Array<Int>();
                
                //Also check to see if the fleet waypoints target a fleet that
                //the player couldn't know about.
                var fleetData = hostFile.getLatestTurn()
                        .playerData[orderSet.player.id].fleets;
                
                //Also check the production queues to see if additional
                //resources were added to the [contributed] value
                var starData = hostFile.getLatestTurn()
                        .playerData[orderSet.player.id].stars.stars;
                        
                for (manipulation in orderSet.fleetManipulations) {
                    FleetManipulations.check(player, manipulation.location, 
                                            manipulation.newFleets);
                }
                
                for (o in orderSet.orders) {
                    switch(o) {
                        
                        case SendMessage(m):
                            //do nothing
                            
                        case ChangeProductionQueue(s, pq):
                            //throw an error if the star isn't owned by the 
                            //player
                            var star = hostFile.getLatestTurn().stars
                                    .getStarById(s.id);
                            if (star.owner.id != player.id) {
                                throw "Cannot change the production queue of a"
                                        + " star that isn't owned by the "
                                        + "player.";
                            }
                            
                            if (arrayContains(foundStars, s.id)) {
                                throw "Tried to set the production queue for " 
                                        + "a star twice.";
                            } else foundStars.push(s.id);
                            
                            //check the production queue
                            var oldPQ = star.production;
                            if (oldPQ == null) {
                                //TODO: This won't create any errors will it?
                                oldPQ = new ProductionQueue(star);
                            }
                            if(!ProductionQueue.checkContributions(oldPQ, pq)){
                                throw "Player " + player.id + " was "
                                        + "attempting to cheat by "
                                        + "changing the resources and minerals"
                                        + " contributed to a production " 
                                        + "queue.";
                            }
                            
                        case ChangeFleetWaypoints(f, waypoint0, waypoints):
                            //throw an error if the fleet isn't owned by the 
                            //player
                            var fleet = hostFile.getLatestTurn().fleets
                                    .getById(f.id, f.owner.id);
                            if (fleet.owner.id != player.id) {
                                throw "Cannot change the waypoints of a fleet "
                                        + "that isn't owned by the player.";
                            }
                            
                            if (arrayContains(foundFleets, f.id)) {
                                throw "Tried to set the waypoints for a fleet " 
                                        + "twice.";
                            } else foundFleets.push(f.id);
                            
                            //check waypoints.
                            for (waypoint in waypoints) {
                                switch(waypoint.target) {
                                    
                                    case Location(v):
                                        //TODO: Check to make sure this is
                                        //inside the map.
                                    case FleetTarget(f):
                                        if (f.id < 0) {
                                            throw "Fleet id cannot be less than 0.";
                                        }
                                        if (fleetData.getById(f.id, f.owner.id) 
                                                == null) {
                                            throw "Tried to set a waypoint at "
                                                    + "an unknown fleet.";
                                        }
                                    
                                    case StarTarget(s):
                                        //ok
                                }
                            }
                            
                        case ChangeResearch(field, percent):
                            if (foundResearch) 
                                throw "Tried to set the research field twice.";
                            else foundResearch = true;
                            
                        case ChangeDesigns(d):
                            if (foundDesigns) 
                                throw "Tried to set the designs twice.";
                            else foundDesigns = true;
                            
                        case ChangeRelations(r):
                            if (r.parent.id != player.id) {
                                throw "Relations parent must be the same as " +
                                      "the player who submitted the turn " + 
                                      "file.";
                            } 
                            
                            if (foundRelations) {
                                throw "Tried to set player relations twice."; 
                            } else foundRelations = true;
                            
                         case ChangeBattlePlan(fleet, design, bp):
                            if (fleet == null) {
                                throw "Cannot change bp of nonexistant fleet.";
                            }
                            if (fleet.owner.id != player.id) {
                                throw "Cannot change bp of a fleet not owned.";
                            }
                            
                            var foundStack = false;
                            for (stack in fleet.ships) {
                                if (stack.type.name == design.name) {
                                    foundStack = true;
                                    break;
                                }
                            }
                            if (!foundStack) {
                                throw "No such design: " + design.name;
                            }
                            
                            if(bp == null) throw "Battle plan can't be null.";
                            
                            for (fpb in foundBattlePlan) {
                                if (fpb.fleet.id == fleet.id && 
                                        fpb.design.isEqualTo(design)) {
                                    throw "Tried to set bp twice.";
                                }
                            }
                            foundBattlePlan.push(
                                    {fleet : fleet, design : design});
                         
                         case ChangeDefaultBattlePlan(bp):
                            if (foundDefaultBP) {
                                throw "Tried to set default bp twice.";
                            }
                            foundDefaultBP = true;
                            
                            if (bp == null) {
                                throw "Battle plan cannot be null.";
                            }
                            
                            //bp name is just set to Default automatically here
                            bp.name = "Default";
                         
                         case ChangeBattlePlans(battlePlans):
                            if (foundBattlePlans) {
                                throw "Tried to set battle plans twice.";
                            }
                            foundBattlePlans = true;
                            
                            var names = new Array<String>();
                            for (bp in battlePlans) {
                                if (bp == null) {
                                    throw "Battle plan cannot be null";
                                }
                                
                                if (bp.name == null) {
                                    throw "Battle plan name can't be null.";
                                }
                                
                                for(name in names) {
                                    if (name == bp.name) {
                                        throw "Cannot use same bp name twice.";
                                    }
                                }
                                names.push(bp.name);
                            }
                        
                    }
                }
                
            } catch (e : Dynamic) {
                throw "Error while parsing order set for player " 
                        + orderSet.player.id + ": " + e;
            }
        }
    }
    
    
    private static function applyOrders() {
        for (orderSet in orders) {
            var player = orderSet.player;
            try {
                for (manipulation in orderSet.fleetManipulations) {
                    FleetManipulations.apply(player, manipulation.location, 
                                            manipulation.newFleets);
                }
                
                for (order in orderSet.orders) {
                    switch(order) {
                        
                        case SendMessage(m):
                            Global.messages.sendMessage(m);
                            
                        case ChangeProductionQueue(s, pq):
                            Global.stars.getStarById(s.id).production = pq;
                            
                        case ChangeFleetWaypoints(f, wp0, waypoints):
                            var fleet = Global.fleets.getById(f.id, player.id);
                            fleet.waypoint0 = wp0;
                            fleet.waypoints = waypoints.copy();
                            
                        case ChangeResearch(field, percent):
                            player.changeResearchField(field, percent);
                            
                        case ChangeDesigns(ds):
                            changeDesigns(player, ds);
                            
                        case ChangeRelations(r):
                            player.relations = r;
                            
                        case ChangeBattlePlan(fleet, design, bp):
                            for (stack in fleet.ships) {
                                if (stack.type.name == design.name) {
                                    stack.battlePlan = bp;
                                }
                            }
                            
                        case ChangeDefaultBattlePlan(bp):
                            player.defaultBattlePlan = bp;
                            
                        case ChangeBattlePlans(battlePlans):
                            player.battlePlans = battlePlans;
                        
                    }
                }
            } catch (e : Dynamic) {
                throw "Error while applying player " + player.id + "'s orders.";
            }
        }
    }
    
    //when a player changes designs, the fleets with ship designs that have 
    //been removed need to have the ships with those designs removed, as well
    //as starabases
    private static function changeDesigns(player : Player, 
            designs : Array < HullDesign > ) {
                
        for (f in Global.fleets) {
            if (f.owner.id != player.id) continue;
            
            //check to see if it contains any ships that are not in designs.
            //go backwards through the array so that stacks can be deleted
            //while looping.
            var i = f.ships.length;
            while (i-- > 0) {
                var found = false;
                for (d in designs) {
                    if (d.isEqualTo(f.ships[i].type)) {
                        found = true;
                        break;
                    }
                }
                
                if (!found) {
                    f.ships.splice(i, 1);
                    //if the fleet no longer has any ships, then remove it.
                    //TODO: this will throw some errors if a player has 
                    //targeted a ship removed this way.
                    if (f.ships.length == 0) {
                        Global.fleets.removeParticle(f);
                        break;
                    }
                }
            }
        }
    }
    
    private static function arrayContains<T>(array : Array<T>, 
            value : T) : Bool {
        for (v in array) {
            if (v == value) return true;
        }
        return false;
    }
}
